#region Using

using System;
using System.IO;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using System.Collections.Generic;
using System.ComponentModel;
using BlogEngine.Core.StorageProviders;

#endregion

namespace BlogEngine.Core
{
	/// <summary>
	/// Representation of a Photo.
	/// </summary>
	/// <remarks>
	/// The WaterMark code is based on the code sample by Joel Neubeck.  You can see the original
	/// article on codeproject at: http://www.codeproject.com/csharp/watermark.asp. Our thanks to 
	/// Joel for his article - it made our implementation a lot easier.
	/// </remarks>
	[Serializable]
	public class Photo : MediaBase<Photo>, IComparable<Photo>
	{
		static private int[] JPEGMagicNumber = { 255, 216 }; // ff d8 ff e0, , 255, 224
		static private int[] GIF89aMagicNumber = { 71, 73, 70, 56, 57, 97 }; // 47 49 46 38 39 61
		static private int[] GIF87aMagicNumber = { 71, 73, 70, 56, 55, 97 }; // 47 49 46 38 37 61
		static private int[] PNGMagicNumber = { 137, 80, 78, 71 }; // 89 50 4e 47

		static public Photo EmptyPhoto = GetEmptyPhoto();

		#region Constructor

		/// <summary>
		/// The default constructor assign default values.
		/// </summary>
		public Photo()
			: base(MediaType.Photo)
		{
		}

		#endregion

		#region Methods

		/// <summary>
		/// Returns all photos in the given album.
		/// </summary>
		public static List<Photo> GetByAlbum(Album album)
		{
			List<Photo> list = Photo.Items.FindAll(delegate(Photo item)
			{
				return item.Album.Id.Equals(album.Id);
			});

			return list;
		}

		/// <summary>
		/// Returns all photos in the given album.
		/// </summary>
		public static List<Photo> GetByAlbum(string albumid)
		{
			return string.IsNullOrEmpty(albumid) ? new List<Photo>() : GetByAlbum(Album.GetById(albumid));
		}

		/// <summary>
		/// Returns all photo in the given album by page
		/// </summary>
		public static List<Photo> GetByPhotoAlbumPage(Guid albumid, int page)
		{
			/// todo: cache results, and re-use
			List<Photo> list = new List<Photo>();

			int perpage = BlogSettings.Instance.AlbumColumns * BlogSettings.Instance.AlbumRows;
			int skip = (page - 1) * perpage;
			int cnt = 0;

			foreach (Photo photo in Photo.Items)
				if (photo.Album.Id.Equals(albumid))
				{
					cnt++;
					if (cnt >= skip)
						list.Add(photo);

					if (list.Count == perpage)
						break;
				}

			return list;
		}

		/// <summary>
		/// Returns all photo in the given album page
		/// </summary>
		[DataObjectMethodAttribute(DataObjectMethodType.Select, true)]
		public static List<Photo> GetByPhotoAlbumPage(string albumid, int page)
		{
			return string.IsNullOrEmpty(albumid) ? new List<Photo>() : GetByPhotoAlbumPage(new Guid(albumid), page);
		}

		/// <summary>
		/// Gets the URL of a sized photo image
		/// </summary>
		public string GetSizedPhotoURL(int size)
		{
			return PhotoURL + "&size=" + size.ToString() + "&last=" + this.LastModified.Ticks.ToString();
		}

		#endregion

		#region Properties

		private Album _Album;
		/// <summary>
		/// Gets or sets the album.
		/// </summary>
		/// <value>The album.</value>
		public Album Album
		{
			get
			{
				return _Album;
			}
			set
			{
				if (_Album != value) MarkChanged("Album");
				_Album = value;
			}
		}

		private StateList<PhotoEffect> _PhotoEffects = new StateList<PhotoEffect>();
		/// <summary>
		/// Gets the photo effects.
		/// </summary>
		public StateList<PhotoEffect> PhotoEffects
		{
			get
			{
				return _PhotoEffects;
			}
		}

		/// <summary>
		/// Gets all the photo effects including those on the album.
		/// </summary>
		public StateList<PhotoEffect> AllPhotoEffects
		{
			get
			{
				StateList<PhotoEffect> all = new StateList<PhotoEffect>();

				foreach (PhotoEffect photoeffect in Album.PhotoEffects)
					all.Add(photoeffect);

				foreach (PhotoEffect photoeffect in PhotoEffects)
					all.Add(photoeffect);

				return all;
			}
		}

		private DateTime _PhotoDate;
		/// <summary>
		/// Gets or sets the date of the photo.
		/// </summary>
		public DateTime PhotoDate
		{
			get
			{
				return _PhotoDate;
			}
			set
			{
				if (_PhotoDate != value) MarkChanged("PhotoDate");
				_PhotoDate = value;
			}
		}

		/// <summary>
		/// Gets the URL of the photo image
		/// </summary>
		protected string PhotoURL
		{
			get
			{
				return Utils.ConvertToAbsolute(string.Format("{0}photo.axd?photo={1}", Utils.RelativeWebRoot, Id.ToString())).ToString();
			}
		}
		/// <summary>
		/// Gets the URL of the photo image
		/// </summary>
		public string DefaultPhotoURL
		{
			get
			{
				return GetSizedPhotoURL(BlogSettings.Instance.DefaultPhotoSize);
			}
		}
		/// <summary>
		/// Gets the Thumbnail URL with the size based on the global settings.
		/// </summary>
		public string AlbumThumbNailURL
		{
			get
			{
				return GetSizedPhotoURL(BlogSettings.Instance.AlbumThumbNailSize);
			}
		}
		/// <summary>
		/// Gets the Thumbnail URL with the size based on the global settings.
		/// </summary>
		public string GalleryThumbNailURL
		{
			get
			{
				return GetSizedPhotoURL(BlogSettings.Instance.GalleryThumbNailSize);
			}
		}

		/// <summary>
		/// Gets the Thumbnail URL with the size based on the global settings.
		/// </summary>
		public string ThumbNailURL
		{
			get
			{
				return GetSizedPhotoURL(BlogSettings.Instance.SlideShowPhotoSize);
			}
		}
		/// <summary>
		/// Gets the extension/format of the photo.
		/// </summary>
		public string Extension
		{
			get { return Location.Substring(Location.LastIndexOf(".") + 1).ToUpperInvariant(); }
		}


		private int _Width;
		/// <summary>
		/// Gets or sets the width of the photo.
		/// </summary>
		public int Width
		{
			get
			{
				return _Width;
			}
			set
			{
				if (_Width != value) MarkChanged("Width");
				_Width = value;
			}
		}

		private int _Height;
		/// <summary>
		/// Gets or sets the height of the photo.
		/// </summary>
		public int Height
		{
			get
			{
				return _Height;
			}
			set
			{
				if (_Height != value) MarkChanged("Height");
				_Height = value;
			}
		}

		private long _FileSize;
		/// <summary>
		/// Gets or sets the file size of the photo.
		/// </summary>
		public long FileSize
		{
			get
			{
				return _FileSize;
			}
			set
			{
				if (_FileSize != value) MarkChanged("FileSize");
				_FileSize = value;
			}
		}

		private string _MD5Hash;
		/// <summary>
		/// Gets or sets the MD5 Hash of the photo file.
		/// Used to detect duplicate files
		/// </summary>
		public string MD5Hash
		{
			get
			{
				return _MD5Hash;
			}
			set
			{
				if (_MD5Hash != value) MarkChanged("Hash");
				_MD5Hash = value;
			}
		}

		/// <summary>
		/// Gets if the item has been changed.
		/// </summary>
		public override bool IsChanged
		{
			get
			{
				if (base.IsChanged)
					return true;

				if (PhotoEffects.IsChanged)
					return true;

				return false;
			}
		}

		/// <summary>
		/// Gets the date of the photo was last updated (including when photo effects were applied)
		/// </summary>
		public DateTime LastModified
		{
			get
			{
				DateTime last = DateModified;

				foreach (PhotoEffect photoeffect in PhotoEffects)
				{
					if (photoeffect.DateModified > last)
						last = photoeffect.DateModified;
					if (photoeffect.DateCreated > last)
						last = photoeffect.DateCreated;
				}

				if (Album != null)
				{
					if (Album.DateModified > last)
						last = Album.DateModified;

					foreach (PhotoEffect photoeffect in Album.PhotoEffects)
					{
						if (photoeffect.DateModified > last)
							last = photoeffect.DateModified;
						if (photoeffect.DateCreated > last)
							last = photoeffect.DateCreated;
					}
				}

				return last;
			}
		}

		#endregion

		#region Methods

		/// <summary>
		/// Gets the URL of a sized photo image
		/// </summary>
		public string GetThumbSizedPhotoURL
		{
			get
			{
				return GetSizedPhotoURL(BlogSettings.Instance.AlbumThumbNailSize);
			}
		}

		/// <summary>
		/// prefix to use for relative photoeffect URL
		/// </summary>
		/// <returns></returns>
		protected override string RelativeLinkPrefix()
		{
			return "photo/";
		}

		/// <summary>
		/// prefix to use for permaphotoeffect URL
		/// </summary>
		/// <returns></returns>
		protected override string PermaLinkPrefix()
		{
			return "photo";
		}

		/// <summary>
		/// Retrieves the photo at its full size.
		/// </summary>
		public Image Image()
		{
			return System.Drawing.Image.FromStream(ImageAsStream());
		}

		/// <summary>
		/// Retrieves a stream containing the photo.
		/// </summary>
		public Stream ImageAsStream()
		{
			//ask the provider for the image stream - we don't know where the file actually comes from
			Stream myImageStream = Provider.Load(this);

			//return the stream
			return myImageStream;
		}

		/// <summary>
		/// Returns a photo object which represents an empty photo. Used as a placeholder.
		/// </summary>
		public static Photo GetEmptyPhoto()
		{
			Photo photo = new Photo();
			photo.Id = Guid.Empty;
			photo.Location = "pics/nophoto.jpg";
			photo.DateModified = DateTime.Now.AddHours(-1);
			return photo;
		}

		/// <summary>
		/// Saves the object to the database.
		/// </summary>
		public new void Save()
		{
			DateModified = DateTime.Now;
			base.Save();

			if (IsDeleted)
				StorageProviderService.PhotoProvider.Delete(this);
		}

		#endregion

		#region Data Access

		internal static BlogEngine.Core.Provider.Interfaces.IStorable<Photo> PhotoProvider = BlogEngine.Core.Provider.BlogService.Provider.GetInterface<BlogEngine.Core.Provider.Interfaces.IPhotos>();

		protected override void DataDelete()
		{
			StorageProviderService.PhotoProvider.Delete(this);
			PhotoProvider.Delete(this);
		}

		protected override void DataInsert()
		{
			PhotoProvider.Insert(this);
		}

		protected override Photo DataSelect(Guid id)
		{
			return PhotoProvider.Select(id);
		}

		protected override void DataUpdate()
		{
			PhotoProvider.Update(this);
		}

		private static int Dummy = OverrideOnLoad();

		private static int OverrideOnLoad()
		{
			OnLoad = PhotoProvider.Fill;
			return 0;
		}

		#endregion

		#region Sort Methods

		/// <summary>
		/// Swaps two albums sort order positions
		/// </summary>
		static public void SwapOrder(Photo photo1, Photo photo2)
		{
			if ((photo1 == photo2) || (photo1 == null) || (photo2 == null))
				return;

			if (photo1.Album != photo2.Album)
				return;

			// if order numbers are the same, reinitalize all the order numbers
			// required to avoid infinite loops based on the assumption that
			// order numbers are unique
			if (photo1.Order == photo2.Order)
				ReInitalizeOrderNumbers(photo1.Album);

			int order = photo1.Order;

			photo1.Order = photo2.Order;
			photo2.Order = order;

			photo1.Save();
			photo2.Save();
			Photo.Items.Sort();
		}

		/// <summary>
		/// Moves given album up one position
		/// </summary>
		static public void MoveUp(Photo photo)
		{
			SwapOrder(photo, photo.Previous);
		}

		/// <summary>
		/// Moves given album down one position
		/// </summary>
		static public void MoveDown(Photo photo)
		{
			SwapOrder(photo, photo.Next);
		}

		/// <summary>
		/// Moves given album to the first position
		/// </summary>
		static public void MoveFirst(Photo photo)
		{
			// slow, but works
			while (!((photo.Previous == null) || (photo.Album != photo.Previous.Album)))
				MoveUp(photo);
		}

		/// <summary>
		/// Moves given album to the last position
		/// </summary>
		static public void MoveLast(Photo photo)
		{
			// slow, but works
			while (!((photo.Next == null) || (photo.Album != photo.Next.Album)))
				MoveDown(photo);
		}

		/// <summary>
		/// Gets the next available sort order number
		/// </summary>
		static public int GetNextOrder(Album album)
		{
			int maxorder = 1;

			foreach (Photo photo in Photo.Items)
				if ((photo.Order >= maxorder) && (photo.Album == album))
					maxorder = album.Order;

			return maxorder + 1;
		}

		/// <summary>
		/// reinitalized all the sort order numbers
		/// </summary>
		static public void ReInitalizeOrderNumbers(Album album)
		{
			List<Photo> list = new List<Photo>();

			foreach (Photo photo in Photo.Items)
				if (photo.Album == album)
					list.Add(photo);

			for (int i = 0; i < list.Count; i++)
			{
				if (list[i].Order != i)
				{
					list[i].Order = i;
					list[i].Save();
				}
			}

			Photo.Items.Sort();
		}

		#endregion

		/// <summary>
		/// This method imports all the files in the import directory
		/// This is a fast way to import lots of photo images, if you access to the web server
		/// And very useful during initial coding and testing
		/// </summary>	
		public static bool BulkImport(Album album, string tagstring, bool delete)
		{
			string folder = System.Web.HttpContext.Current.Server.MapPath(BlogSettings.Instance.StorageLocation + System.IO.Path.DirectorySeparatorChar + "bulkimport");
			DirectoryInfo di = new DirectoryInfo(folder);

			return ImportPhotos(di, album, tagstring, delete);
		}

		/// <summary>
		/// This method imports all the files in the given directory
		/// </summary>
		private static bool ImportPhotos(DirectoryInfo di, Album album, string tagstring, bool delete)
		{
			foreach (FileInfo f in di.GetFiles("*.*", SearchOption.TopDirectoryOnly))
				if (!ImportPhoto(f, album, tagstring, delete))
					return false;

			return true;
		}

		/// <summary>
		/// This method import a file on the file system
		/// </summary>
		private static bool ImportPhoto(FileInfo fi, Album album, string tagstring, bool delete)
		{
			Photo photo = new Photo();
			photo.Album = album;
			photo.TagString = tagstring;

			Stream file = fi.OpenRead();

			try
			{
				if (!photo.Import(file, fi.Name))
					return false;
			}
			finally
			{
				file.Close();
			}

			photo.Save();

			if (delete)
				fi.Delete();

			return true;
		}

		/// <summary>
		/// This method returns a MD5 hash of a stream
		/// </summary>
		private static string GetHash(Stream file)
		{
			System.Security.Cryptography.MD5CryptoServiceProvider sha1 = new System.Security.Cryptography.MD5CryptoServiceProvider();
			file.Seek(0, SeekOrigin.Begin);
			byte[] result = sha1.ComputeHash(file);

			StringBuilder sb = new StringBuilder();
			for (int i = 0; i < result.Length; i++)
				sb.Append(result[i].ToString("X2"));

			return sb.ToString();
		}

		/// <summary>
		/// This method import a file
		/// </summary>
		public bool Import(Stream file, string originalfilename)
		{
			OriginalFileName = System.IO.Path.GetFileName(originalfilename);

			if (!IsSupportedPhotoFile(file))
				return false;

			this.PhotoDate = DateTime.Now;

			SetMetaData(file, this, true);

			try
			{
				StorageProviderService.PhotoProvider.Save(this, file);
				return true;
			}
			catch (Exception e)
			{
				return false;
			}
		}

		/// <summary>
		/// Updates the photo meta data with information from the photo file
		/// </summary>
		private static void SetMetaData(Stream file, Photo photo, bool usetagdate)
		{
			int height = 0;
			int width = 0;

			GetHeightAndWidth(file, ref height, ref width);

			photo.Width = width;
			photo.Height = height;
			photo.FileSize = file.Length;
			photo.MD5Hash = GetHash(file);

			if ((usetagdate) && (IsJPEG(file)))
			{
				Dictionary<int, string> tags = GetJPEGTags(file);

				if (tags.ContainsKey(306))
				{
					DateTime photodate = NormalizeJPEGDate(tags[306]);

					if (photodate != DateTime.MinValue)
						photo.PhotoDate = photodate;
				}
			}
		}

		/// <summary>
		/// Updates the all photo meta data		
		/// </summary>
		public static void UpdateAllMetaData()
		{
			List<Photo> copy = new List<Photo>();

			foreach (Photo photo in Photo.Items)
				copy.Add(photo);

			foreach (Photo photo in copy)
			{
				Stream file = StorageProviderService.PhotoProvider.Load(photo);
				SetMetaData(file, photo, true);
				photo.Save();
			}
		}

		/// <summary>
		/// Checks for duplicate photo
		/// </summary>
		public bool IsDuplicate()
		{
			foreach (Photo chk in Photo.Items)
				if ((chk.FileSize == this.FileSize) && (chk.MD5Hash == this.MD5Hash) && (chk != this))
					return true;

			return false;
		}

		#region Helper Functions

		/// <summary>
		/// Determines if file has a particular magic number
		/// </summary>
		private static bool HasMagicNumber(Stream file, int[] key)
		{
			if (file.Length < key.Length)
				return false;

			file.Seek(0, SeekOrigin.Begin);

			foreach (int keyvalue in key)
				if (file.ReadByte() != keyvalue)
					return false;

			return true;
		}

		/// <summary>
		/// Determines if file is a jpeg file
		/// </summary>
		private static bool IsJPEG(Stream file)
		{
			return HasMagicNumber(file, JPEGMagicNumber);
		}

		/// <summary>
		/// Determines if file is a jpeg file
		/// </summary>
		private static bool IsSupportedPhotoFile(Stream file)
		{
			if (HasMagicNumber(file, JPEGMagicNumber))
				return true;
			if (HasMagicNumber(file, GIF89aMagicNumber))
				return true;
			if (HasMagicNumber(file, GIF87aMagicNumber))
				return true;
			if (HasMagicNumber(file, PNGMagicNumber))
				return true;

			return false;
		}

		/// <summary>
		/// This method reads a tag from a JPEG file
		/// </summary>
		static protected Dictionary<int, string> GetJPEGTags(Stream file)
		{
			Dictionary<int, string> tags = new Dictionary<int, string>();

			try
			{
				file.Seek(0, SeekOrigin.Begin);
				System.Drawing.Image image = new Bitmap(file);

				foreach (int id in image.PropertyIdList)
				{
					PropertyItem item = image.GetPropertyItem(id);
					string tag = string.Empty;

					switch (item.Type)
					{
						case 2: for (int i = 0; i < item.Len - 1; i++) tag = tag + (char)item.Value[i]; break;
						case 3: tag = System.BitConverter.ToUInt16(item.Value, 0).ToString(); break;
						case 4: tag = System.BitConverter.ToUInt32(item.Value, 0).ToString(); break;
						case 7: tag = System.BitConverter.ToInt32(item.Value, 0).ToString(); break;
						default: break;
					}

					if (tag.Length == 0)
						tags.Add(item.Id, tag);
				}

				return tags;
			}
			catch
			{
				return tags;
			}
		}

		/// <summary>
		/// This method reads a tag from a JPEG file
		/// </summary>
		private static string GetJPEGTag(Stream file, int tagnbr)
		{
			try
			{
				file.Seek(0, SeekOrigin.Begin);
				System.Drawing.Image image = new Bitmap(file);

				foreach (int id in image.PropertyIdList)
				{
					PropertyItem item = image.GetPropertyItem(id);

					if (item.Id == tagnbr)
					{
						string tag = "";
						for (int i = 0; i < (item.Len - 1); i++)
						{
							char c = (char)item.Value[i];
							if (c != '\n')
								tag = tag + (char)item.Value[i];
						}

						return tag;
					}
				}

				return null;
			}
			catch
			{
				return null;
			}
		}

		/// <summary>
		/// This method normalize a JPEG date/time tag
		/// </summary>
		private static DateTime NormalizeJPEGDate(string date)
		{
			if (string.IsNullOrEmpty(date))
				return DateTime.MinValue;

			DateTime jpegdate = DateTime.MinValue;

			if (DateTime.TryParse(date, out jpegdate))
				return jpegdate;

			if (DateTime.TryParse(date.Substring(5, 2) + '/' + date.Substring(8, 2) + '/' + date.Substring(0, 4) + ' ' + date.Substring(11), out jpegdate))
				return jpegdate;

			return DateTime.MinValue;
		}
		/// <summary>
		/// This method gets the height and width of the image file
		/// </summary>
		private static void GetHeightAndWidth(Stream file, ref int height, ref int width)
		{
			try
			{
				file.Seek(0, SeekOrigin.Begin);
				System.Drawing.Image image = new Bitmap(file);
				height = image.Height;
				width = image.Width;
			}
			catch
			{
			}
		}

		#endregion

		#region IComparable<Photo> Members

		/// <summary>
		/// Compares the current object with another object of the same type.
		/// </summary>
		/// <param name="other">An object to compare with this object.</param>
		/// <returns>
		/// A 32-bit signed integer that indicates the relative order of the 
		/// objects being compared. The return value has the following meanings: 
		/// Value Meaning Less than zero This object is less than the other parameter.Zero 
		/// This object is equal to other. Greater than zero This object is greater than other.
		/// </returns>
		public int CompareTo(Photo other)
		{
			if (this.Album == other.Album)
				return CompareOrderAndTitle(this.Order, this.PhotoDate, this.Title, other.Order, other.PhotoDate, other.Title);
			else
				return this.Album.Id.CompareTo(other.Album.Id);
		}

		#endregion


	}
}

